Repository navigation
feat(pr-pipeline): run the capture's driven Claude Code behind the key proxy in a sibling sandbox - #13684
Conversation
…y proxy in a sibling sandbox The capture job starts the key proxy as its own OS user with the key on stdin before isolating the runner, installs the driven Claude Code the lock pins and a root-owned driver with its engine, then starts a no-network sibling sandbox under an env -i allowlist. Inside it, sibling-init.sh runs the reach probe (which must miss a sentinel credential planted outside), records the sandbox's facts and starts the launch broker on a dummy key; "Check the sibling sandbox" fails the job on anything unexpected before any pull request code. The driver step reaches the broker through CCD_* names. The sandbox is stopped before collect, which now copies no symlink. capture-subscription reads no secret and runs no model: after the capture job's gates it uploads a capture recorded blocked, since a subscription token cannot go behind the proxy. capture-fallback is removed with its lane-funding rows. merge.verify-capture.proxy carries the proxy's limits and allowlists; merge.verify-capture.app stays unset until the runner probe. callers.test.mjs pins the key proxy start as the one non-launcher step that may read the key, and exempts the capture lane from the subscription and fallback legs permanently: the owner chose key-only capture, and a subscription token may not be routed through a proxy we run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its count_tokens preflight A /v1/messages request whose max_tokens alone takes committed and reserved tokens past the cap now gets the spend-limit answer with no upstream call, so a spent budget makes no further count_tokens requests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the proxy on every path The sandbox now binds the workspace, the head and main's checkout read-only and only the run directory writable, so pull request code cannot plant configuration the outer driver leg reads; the workflow makes head and capture-run as empty mount points before bwrap. /run is a tmpfs with only the proxy's socket directory bound back, and the sandbox runs in its own user namespace with no capability and no way to make another. sibling-init records name resolution, the unix sockets that accept a connection, where it can write and whether it can make a user namespace, and the check fails the job unless each is as built. The probe prompt adds links in the run directory to /proc/self/environ and the fence file, and the probe check requires both reads refused. The sandbox is stopped whenever it started, failing the job if it survives, and collect copies nothing unless it stopped. A watcher beside the key proxy ends it on a stop file the runner's user creates, since that user cannot signal ccd-proxy; the proxy stop and the usage report run whenever the proxy started. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A messages request refused at reserve still cost the job an upstream count_tokens call, and pull request code could repeat it without end while budget remained. The proxy now counts the requests that reach the preflight against maxPreflightRequests and answers with the spend limit once it is reached. merge.verify-capture.proxy.max-preflight-requests sets it, 1000 for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lanted links The reach probe now holds the driver's Write and Grep tools and its edit allowances. It writes through a run-directory link to a file under ~/.claude that does not exist and greps through the link to /proc/self/environ; the probe check fails the job unless the transcript shows both refused and the Write's target is still absent. sibling-init records dns, userns and the socket scan as untested when getent, unshare or socat is missing, so the check refuses them. Once the sandbox is stopped, the run directory loses its top-level dot entries (.git, .claude, .mcp.json and the like). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…agment pr-pipeline releases from fragments, so the CHANGELOG heading goes, and the hosted runner's /home/runner lines carry the machine-path marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ring # Conflicts: # .github/actions/resolve-config/callers.test.mjs # .github/lane-funding.json
Codex Review Summary: ❌ FailedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
claude-security-review reviewed Security review: PR #13684 (capture key proxy and sibling sandbox)No security issues found. This was a full read of all 11 changed files, every hunk. This lane already reviewed an earlier head ( critical-findings: 0 What I checked
Spend (not a finding, not a hold): the probe's |
|
claude-review reviewed Code review: PR #13684, running the capture's driven Claude Code behind the key proxy in a sibling sandboxResult: no blocking findings. This lane already reviewed an earlier head ( I read every hunk of all 11 files listed in What I checked this round
Credit spend (not a finding)Prepaid API credit only, so no cost hold:
Carried-over note (not counted)The earlier note still applies: the socket scan's |
|
The pr-unblock lane stopped
When a retry on this head can change the outcome, the lane runs one on its own, up to its per-head cap, and edits this comment if it stops again; a run that does not stop clears the status. When no retry can (a review thread the lane does not take or settle, or a failing check after the retries are spent), a person resolves the threads or gets the checks green, then dispatches the workflow below: that run finds the cause gone and clears the status. On a pull request the lanes App opened, the dispatchers start that run once per stop; on any other, a person does. A new commit to the branch starts clean. To run it again on this head now, dispatch the gh workflow run pr-unblock.yml -R melodic-software/claude-code-plugins -f pr-number=13684 -f activity=resolve-conflicts |
|
Unattended merge: risk HIGH at head |
|
Unattended merge: risk HIGH at head This refusal has stood unchanged at this head, so a person is needed: the pr-merge lane added the human-gated label. |
…ring # Conflicts: # .github/workflows/pr-verify-capture.yml
|
The pr-merge lane escalated this pull request, and its head has moved ( |
Refs: #12232
Summary
PR C Phase 5 of #12232: wire the key proxy (#13662) and the launch broker (#13661) into
pr-verify-capture.yml, so the UI capture of a hooks or mods change drives the PR's plugin code inside a sibling sandbox that holds only a dummy key. The capture stays off:merge.verify-capture.appis unset until a runner probe (Phase 5b) passes on a hosted runner.Fix
capturejob:ccd-proxyOS user and root-owned/opt/ccd. Installs the Claude Code version the root lock pins, from a pruned lock withnpm ciandnpm audit signatures. Installs and checks the driver engine. Writes the driven managed settings and the proxy config from main'spr-pipeline.yaml. Starts the proxy asccd-proxyunderenv -i, with the key on stdin only.env -i(PATH, HOME, TERM, LANG, CCD_RUNTIME_DIR), with--unshare-pid --proc /proc --unshare-net --unshare-ipc --unshare-uts --unshare-user --disable-userns --cap-drop ALL. tmpfs over/tmp,$HOME,$RUNNER_TEMPand/run, with only the proxy socket directory bound back. The workspace,.baseandheadare read-only; onlycapture-runand the runtime dir are writable.capture-run.CCD_RUNTIME_DIRandCCD_BROKER_SOCK.capture-subscriptionreads no secret and runs no model: after its gates it uploads a capture recordedblocked(reason: a subscription token cannot go behind the key proxy, R41).capture-fallbackis removed, with itslane-funding.jsonrows, and a permanent leg exemption incallers.test.mjscites the owner's key-only decision (ci(pr-verify): add the capture job that drives the affected app (S7 of #8651) #12232 Q5).merge.verify-capture.proxyinpr-pipeline.yamland the schema: token cap, models, max_tokens ceiling, body size, in-flight limit, count_tokens and preflight limits, upstream timeout, and the beta, body-key and block-type allowlists measured on Claude Code 2.1.318. A test tiesclaude-code-versionto the lock pin, so a Claude Code bump fails until the betas are re-measured.key-proxy.mjs: a request is refused before its count_tokens preflight once the cap is spent or a per-job preflight limit is reached.pr-pipelinechangelog fragment.Security & Cost
callers.test.mjs's credential rule gets a narrow, shape-pinned exception for it. No PR code runs in any process that holds the key or above it.ccd-dummy-key. It can spend promotional key credit only through the proxy, bounded by the per-job token cap and request limits (the owner's option A, ci(pr-verify): add the capture job that drives the affected app (S7 of #8651) #12232 issuecomment-6671043587).PR_VERIFY_CLAUDE_API_KEY's promotional credit and recordsblockedotherwise (owner Q5). No new runners or paid services.Verification
node --test .github/scripts/pr-verify/workflow.test.mjs: 73/73..github/actions/resolve-config/callers.test.mjs: 264/264..github/scripts/pr-verify/key-proxy.test.mjs: 40/40. shellcheck and actionlint clean;check-jsonschemaonpr-pipeline.yamlok;scripts/check-affected.sh12 passed./runempty, DNS refused, user namespaces refused, no sockets but the proxy's. Not verifiable here, left to Phase 5b on ubuntu-24.04: the pruned-locknpm ci, bubblewrap's support for--disable-userns, that actions/checkout fills the pre-madeheadthe sandbox binds read-only, and the full proxy, sandbox and broker run.Related
merge.verify-capture.app.Follow-ups
ifstrings, launcher counts), which makes them change detectors.cp -Pin collect guards only the last path component; the sandbox is stopped before collect, which bounds it.Bashto the driver and git commands it needs, since it reads forgeable sandbox output.🤖 Generated with Claude Code